Skip to content

fix(hooks): register configured hooks in TUI gateway and serve backends - #111315

Open
wesleymatosdev wants to merge 1 commit into
NousResearch:mainfrom
wesleymatosdev:fix/tui-hook-registration-v2
Open

wesleymatosdev wants to merge 1 commit into
NousResearch:mainfrom
wesleymatosdev:fix/tui-hook-registration-v2

Conversation

@wesleymatosdev

Copy link
Copy Markdown

Problem

Hooks and webhooks configured in config.yaml fire on hermes --cli (via hermes_cli.main._prepare_agent_startup) and on the messaging gateway (gateway/run.py), but never from the TUI gateway backends:

  • hermes --tui (stdio backend, tui_gateway/entry.py)
  • dashboard chat PTY (same backend)
  • desktop WS sidecar (tui_gateway/ws.py)
  • hermes serve / dashboard backend (hermes_cli/web_server.py)

A user who configures a shell hook or outbound webhook gets silent behavior differences depending on which entry point their session happens to be driven through. This is also why the fix keeps landing piecemeal: #67084, #78805, #81409, #83997, #87116, #92655, #98889 (and #34112 / #41464) each patch one path.

Fix

New agent/hook_registration.ensure_hooks_registered() — once-per-process guard, never raises:

  • tui_gateway/entry.py main() and tui_gateway/ws.py handle_ws register via a thin tui_gateway/server._register_hooks_from_config() delegate
  • hermes_cli/web_server.py _lifespan registers at serve startup

Consent semantics are unchanged (still owned by agent.shell_hooks: flag / env / config opt-in, fail-closed on non-TTY stdin — neither entry point prompts on piped stdio). Fail-soft: a broken hook config can't take down a backend. The existing inline registrations in the CLI and gateway behave identically and can migrate to the shared helper later without behavior change.

Testing

tests/tui_gateway/test_hook_registration_parity.py — 7 tests covering: once-per-process guard, entry-point wiring (entry / ws / web_server), consent passthrough, and fail-soft behavior.

tests/tui_gateway/test_hook_registration_parity.py  7 passed

Supersedes the single-path PRs listed above.

Every long-lived agent runtime must register the user's shell hooks and
outbound webhooks at startup. The CLI (_prepare_agent_startup) and the
messaging gateway (gateway/run.py) both do; three backends did neither:

* tui_gateway.entry.main — 'hermes --tui' stdio backend (and the
  dashboard chat PTY, which spawns the same entry point)
* tui_gateway.ws.handle_ws — the dashboard / desktop WebSocket sidecar
* hermes_cli.web_server._lifespan — 'hermes serve' / dashboard backend

Net effect: a webhook configured in config.yaml fired in 'hermes --cli'
but silently never fired from a TUI, dashboard, or desktop session —
including the on_session_end event integration points rely on for
completion signaling. Nine open PRs (NousResearch#67084, NousResearch#78805, NousResearch#81409, NousResearch#83997,
whole class instead.

Add agent/hook_registration.ensure_hooks_registered() — once-per-process
guard, shell-hook consent semantics unchanged (flag / env / config
opt-in, fail-closed on non-TTY stdin), outbound registration unchanged,
fail-soft: a broken hook config never takes down a backend. Wire it into
all three backends. The two inline call sites in main.py and gateway/run.py
behave identically and can migrate later without behavior change.

Regression coverage in tests/tui_gateway/test_hook_registration_parity.py:
registration content, explicit-cfg path, once-per-process guard, failure
isolation, and all three entry-point wirings (7 tests).
Copilot AI lite review requested due to automatic review settings September 15, 2026 00:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved profile-scoping, concurrency, plugin-ordering, and slash-worker registration issues remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR registers configured shell hooks and outbound webhooks across TUI, WebSocket, and serve/dashboard backends.

Changes:

  • Adds a shared fail-soft hook registration helper.
  • Wires registration into TUI, WS, and web-server startup.
  • Adds seven parity and failure-isolation tests.
File summaries
File Reviewed changes
tui_gateway/ws.py Adds WebSocket startup wiring for hook registration.
tui_gateway/server.py Adds the registration delegate; profile-specific and slash-worker registration gaps remain.
tui_gateway/entry.py Adds stdio TUI startup wiring.
tests/tui_gateway/test_hook_registration_parity.py Adds registration parity tests; additional profile and worker coverage is needed.
hermes_cli/web_server.py Registers hooks during serve startup.
agent/hook_registration.py Adds shared registration, but requires profile scoping, synchronized completion, and correct plugin-discovery ordering.
Review details

Suppressed comments (3)

agent/hook_registration.py:58

  • Setting _ensured = True before load_config() and both registrations finish makes the guard report completion while work is still in progress. handle_ws invokes this from multiple concurrent WebSocket tasks; a second connection can return here and process its first RPC before the first connection has registered the webhooks. Use an in-progress/completed state (or synchronize on the registration lock) so other callers wait for completion and only skip after registration has finished.
    with _ensured_lock:
        if _ensured:
            return
        _ensured = True

agent/hook_registration.py:67

  • These callbacks are appended before plugin discovery on the new serve/WS startup path: _lifespan calls ensure_hooks_registered() before agent setup discovers plugins. PluginDispatchMixin.invoke_hook preserves _hooks insertion order and pre_tool_call uses the first directive; gateway startup explicitly discovers plugins before config hooks (gateway/run_startup.py:875-910) so plugin policy wins ties. Discover/load the active profile's plugins before registering config callbacks, otherwise a shell block can preempt a plugin policy callback.
        from agent import outbound_webhooks, shell_hooks

        shell_hooks.register_from_config(cfg, accept_hooks=accept_hooks)
        outbound_webhooks.register_from_config(cfg)

tui_gateway/server.py:391

  • The slash worker is a separate subprocess with a fresh hook registry. The hermes serve/dashboard path does not set HERMES_DEFER_AGENT_STARTUP, so its HermesCLI._init_agent() reaches _prepare_deferred_agent_startup() and returns without registering config hooks; the parent registration cannot cross that process boundary. Agent-backed slash commands therefore still miss configured shell hooks and outbound webhooks. Register the worker at startup using its profile_home, or call the helper in tui_gateway.slash_worker, and cover this path.
    from agent.hook_registration import ensure_hooks_registered

    ensure_hooks_registered()
  • Files reviewed: 6/6 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +30 to +31
_ensured_lock = threading.Lock()
_ensured = False
Comment thread tui_gateway/server.py
Comment on lines +389 to +391
from agent.hook_registration import ensure_hooks_registered

ensure_hooks_registered()
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/tui Terminal UI (ui-tui/ + tui_gateway/) comp/dashboard Web dashboard / control panel UI (dashboard/, landing) labels Sep 15, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Related: open single-path PRs for the same hook-registration gap: #98889 (serve/dashboard), #102151 and #102665 (serve), #78805 (dashboard + TUI slash-worker). This PR proposes one shared helper across all backends; reviewers should pick between the consolidated approach here and the per-path fixes.

@kvnloo

kvnloo commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

exact-head c28866f — void if moved

KEEP: shared agent/hook_registration.py + register on TUI entry/server/ws and hermes_cli/web_server.py, with tests/tui_gateway/test_hook_registration_parity.py. Closes the silent gap where config.yaml hooks fire on CLI/gateway but not Desktop/TUI/serve.

PICK-ONE vs open spray (same gap, narrower footprint):

Recommendation: land this PR; close or rebase the spray onto it. Soft note: #53894 (profile-keyed session hooks) overlaps tui_gateway/entry.py+server.py — rebase after, do not auto-close.

CHECK: Copilot called out profile-scoping / concurrency / plugin-ordering — worth a quick pass before merge. Soft file overlap with #111292 on tui_gateway/server.py (live-sessions) — merge order: prefer hooks first or rebase live-sessions.

@xianjinas

Copy link
Copy Markdown

Reproduced on v0.20.2 (2026.8.16), Linux — same root cause, found independently before I saw this PR. Adding the evidence in case it helps get this merged:

1. tui_gateway never registers hooks

$ grep -rn 'shell_hook\|register_from_config' <site-packages>/tui_gateway/
(no matches — the module only pulls in tools.approval.load_permanent_allowlist)

2. The messaging gateway registers the very same hook fine

INFO agent.shell_hooks: shell hook registered: pre_tool_call -> python3 -S /abs/path/guard.py
     (matcher=terminal|write_file|patch|execute_code, timeout=10s, fail_closed=False)

3. In the desktop panel / TUI path the hook is silently a no-op

  • session host process is python3 -m tui_gateway.entry (child of the TUI frontend, child of hermes dashboard)
  • a pre_tool_call hook whose job is to block rm <protected path> lets the command execute
  • the hook script's own JSONL audit log receives no entry at all → the hook was never invoked (not a parsing bug: feeding the identical payload to the hook script directly returns {"action":"block", ...} plus exit code 2)
  • so it fails open and silently

4. Not caching / not config staleness

Full app restart (wrapper + gateway + dashboard + tui_gateway all brand-new PIDs) → gateway sessions protected, panel/TUI still unprotected. Pure code-path gap.

Why this one matters: with hooks_auto_accept: true the operator gets no warning that the guard is inactive on that path. The desktop panel is exactly the surface where a human is watching and where a guard is expected to be a hard stop — a silently inactive hook is worse than an explicit "unsupported on this channel". Agreed with the PR's direction; happy to test a build if it helps.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/dashboard Web dashboard / control panel UI (dashboard/, landing) comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants